﻿using System;
using System.Collections.Generic;
using System.Reflection;
using Pfz.Threading;
using Pfz.Extensions;

namespace Pfz.DynamicObjects
{
	/// <summary>
	/// Class for generating lazy-loaders.
	/// </summary>
	public sealed class LazyLoaderGenerator
	{
		private readonly LazyLoaderGenerator<byte> _generator = new LazyLoaderGenerator<byte>();

		#region MustBeCollectible
			/// <summary>
			/// Identifies if the generated assemblies will be collectible or not.
			/// </summary>
			public bool MustBeCollectible
			{
				get
				{
					return _generator.MustBeCollectible;
				}
				set
				{
					_generator.MustBeCollectible = value;
				}
			}
		#endregion

		/// <summary>
		/// Registers a lazy-loader function for a given T data-type.
		/// Note that all properties of the same type will use the same loader
		/// function, but they will receive the property name as their parameter,
		/// so make sure the property-name is enough to get all the info you
		/// need.
		/// </summary>
		public void RegisterLazyLoaderFor<T>(LazyType lazyType, LazyLoaderFunc<T> loaderFunc)
		{
			if (loaderFunc == null)
				throw new ArgumentNullException("loaderFunc");

			_generator.RegisterLazyLoaderFor<T>(lazyType, (ignored, name) => loaderFunc(name));
		}

		/// <summary>
		/// Gets the creator for a given interface type.
		/// </summary>
		public Func<TInterface> GetCreator<TInterface>()
		{
			var result = _generator.GetCreator<TInterface>();
			
			return () => result(0);
		}
		
		/// <summary>
		/// Creates an instance of the given interface type built of lazy-loaders.
		/// As no parameters are given, there's a big chance you will use it
		/// like a singleton.
		/// </summary>
		public TInterface Create<TInterface>()
		{
			var creator = GetCreator<TInterface>();
			return creator();
		}
	}

	/// <summary>
	/// Class for generating lazy-loaders that receive an user-instance.
	/// </summary>
	public sealed class LazyLoaderGenerator<TUserInstance>
	{
		private readonly Dictionary<Type, KeyValuePair<Delegate, LazyType>> _loaderFuncs = new Dictionary<Type, KeyValuePair<Delegate, LazyType>>();

		#region MustBeCollectible
			/// <summary>
			/// Identifies if the generated assemblies will be collectible or not.
			/// </summary>
			public bool MustBeCollectible { get; set; }
		#endregion

		/// <summary>
		/// Registers a lazy-loader function for a given T data-type.
		/// Note that all properties of the same type will use the same loader
		/// function, but they will receive the property name as their parameter,
		/// so make sure the property-name is enough to get all the info you
		/// need.
		/// </summary>
		public void RegisterLazyLoaderFor<T>(LazyType lazyType, LazyLoaderFunc<TUserInstance, T> loaderFunc)
		{
			if (loaderFunc == null)
				throw new ArgumentNullException("loaderFunc");

			var pair = new KeyValuePair<Delegate, LazyType>(loaderFunc, lazyType);
			_loaderFuncs.Add(typeof(T), pair);
		}

		private static void _Add_Lazy_Property<T>(DelegatedTypeBuilder<TUserInstance> typeBuilder, string name, LazyLoaderFunc<TUserInstance, T> loaderFunc)
		{
			typeBuilder.AddPropertyWithField<T, Lazy<T>>
			(
				name,
				(userInstance, field) => field.Value,
				null,
				(userInstance) => new Lazy<T>(() => loaderFunc(userInstance, name))
			);
		}
		private static void _Add_LazyAndWeak_Property<T>(DelegatedTypeBuilder<TUserInstance> typeBuilder, string name, LazyLoaderFunc<TUserInstance, T> loaderFunc)
		where
			T: class
		{
			typeBuilder.AddPropertyWithField<T, LazyAndWeak<T>>
			(
				name,
				(userInstance, field) => field.Value,
				null,
				(userInstance) => new LazyAndWeak<T>(() => loaderFunc(userInstance, name))
			);
		}
		private static void _Add_BackgroundLoader_Property<T>(DelegatedTypeBuilder<TUserInstance> typeBuilder, string name, LazyLoaderFunc<TUserInstance, T> loaderFunc)
		where
			T: class
		{
			typeBuilder.AddPropertyWithField<T, BackgroundLoader<T>>
			(
				name,
				(userInstance, field) => field.Value,
				null,
				(userInstance) => new BackgroundLoader<T>(() => loaderFunc(userInstance, name))
			);
		}

		private static readonly MethodInfo _add_Lazy_PropertyMethod = typeof(LazyLoaderGenerator<TUserInstance>).GetMethod("_Add_Lazy_Property", BindingFlags.Static | BindingFlags.NonPublic);
		private static readonly MethodInfo _add_LazyAndWeak_PropertyMethod = typeof(LazyLoaderGenerator<TUserInstance>).GetMethod("_Add_LazyAndWeak_Property", BindingFlags.Static | BindingFlags.NonPublic);
		private static readonly MethodInfo _add_BackgroundLoader_PropertyMethod = typeof(LazyLoaderGenerator<TUserInstance>).GetMethod("_Add_BackgroundLoader_Property", BindingFlags.Static | BindingFlags.NonPublic);
		private readonly Dictionary<Type, Delegate> _creators = new Dictionary<Type, Delegate>();
		
		/// <summary>
		/// Gets the creator for a given interface type.
		/// </summary>
		public Func<TUserInstance, TInterface> GetCreator<TInterface>()
		{
			if (!typeof(TInterface).IsInterface)
				throw new ArgumentException("TInterface must be an interface type.");
				
			Delegate untypedResult;
			if (_creators.TryGetValue(typeof(TInterface), out untypedResult))
				return (Func<TUserInstance, TInterface>)untypedResult;
				
			var typeBuilder = new DelegatedTypeBuilder<TUserInstance>(null, MustBeCollectible, typeof(TInterface));
			foreach(var property in typeof(TInterface).GetInterfaceProperties())
			{
				string name = property.Name;
				var propertyType = property.PropertyType;
				
				var loaderFuncPair = _loaderFuncs[propertyType];
				var loaderFunc = loaderFuncPair.Key;
				MethodInfo method;
				switch(loaderFuncPair.Value)
				{
					case LazyType.LazyAndWeak:
						method = _add_LazyAndWeak_PropertyMethod;
						break;
						
					case LazyType.Lazy:
						method = _add_Lazy_PropertyMethod;
						break;
						
					case LazyType.BackgroundLoader:
						method = _add_BackgroundLoader_PropertyMethod;
						break;
						
					default:
						throw new InvalidOperationException("Invalid LazyType.");
				}
				
				method = method.MakeGenericMethod(propertyType);
				var fastMethod = method.GetDelegate();
				fastMethod(null, new object[]{typeBuilder, name, loaderFunc});
			}
			
			var creator = typeBuilder.CreateCreator();
			Func<TUserInstance, TInterface> result =
				(userInstance) =>
				{
					return (TInterface)creator(userInstance);
				};
				
			_creators.Add(typeof(TInterface), result);
			return result;
		}
	}
}
